如何使用 iText 添加 PAdES-LTV

how can I add PAdES-LTV using iText

我正在尝试在没有 LTV 格式的已签名 PDF 文档中启用 LTV。我在所有情况下都找到了相同的示例,如链接 How to enable LTV for a timestamp signatureiText LTV 已启用 - 如何添加更多 CRL? ,其中定义了获得预期结果的程序是什么。碰巧我没有工作,它没有给我任何错误,但我没有添加 LTV。

关于为什么在执行以下代码时没有给我任何错误但我没有添加 LTV 的一些想法。

这是我尝试添加 LTV 的方法:

public void addLtv(String src, String dest, OcspClient ocsp, CrlClient crl, TSAClient tsa)
    throws IOException, DocumentException, GeneralSecurityException {
    PdfReader r = new PdfReader(src);
    FileOutputStream fos = new FileOutputStream(dest);
    PdfStamper stp = PdfStamper.createSignature(r, fos, '[=10=]', null, true);
    LtvVerification v = stp.getLtvVerification();
    AcroFields fields = stp.getAcroFields();
    List<String> names = fields.getSignatureNames();
    String sigName = names.get(names.size() - 1);
    PdfPKCS7 pkcs7 = fields.verifySignature(sigName);
    if (pkcs7.isTsp()) {
        v.addVerification(sigName, ocsp, crl,
            LtvVerification.CertificateOption.SIGNING_CERTIFICATE,
            LtvVerification.Level.OCSP_CRL,
            LtvVerification.CertificateInclusion.NO);
    }
    else {
        for (String name : names) {
            v.addVerification(name, ocsp, crl,
                LtvVerification.CertificateOption.WHOLE_CHAIN,
                LtvVerification.Level.OCSP_CRL,
                LtvVerification.CertificateInclusion.NO);
        }
    }
    PdfSignatureAppearance sap = stp.getSignatureAppearance();
    LtvTimestamp.timestamp(sap, tsa, null);
}

我正在使用的版本:

如评论中所述

i want is Adobe LTV-enable

该任务与 PAdES 的相关性较低(即使使用了 PAdES 中引入的机制),但专注于 Adob​​e 专有签名配置文件,"LTV enabled" signatures.

很遗憾,没有正确指定此专有签名配置文件。 Adobe 告诉我们的只是

LTV enabled means that all information necessary to validate the file (minus root certs) is contained within.

(有关详细信息和背景,请阅读 this answer

因此,实施 LTV 启用 示例签名的方法涉及一些试验和错误,我不能保证 Adob​​e 会接受此代码的输出 "LTV enabled"在即将推出的 Adob​​e Acrobat 版本中。

此外,当前的 iText 5 签名 APIs 不足以开箱即用,因为(事实证明)Adobe 需要某些 iText 代码不创建的其他可选结构(但请参阅下面的 PPS)。解决这个问题的最简单方法是在两个方面更新 iText class LtvVerification,所以我将在此处描述这种方式。或者,可以使用 Java 反射或复制和调整相当多的代码;如果您无法如下所示更新 iText,则必须选择一种此类替代方法。

支持签名 PDF 签名的 LTV

此部分显示了代码添加和更改,LTV 可用于启用文档,例如 OP 的示例 PDF sign_without_LTV.pdf

一种使用 iText 的方法 LtvVerification class

这是使用 iText 签名 API 中的 LtvVerification class 的原始代码。不幸的是,为此必须添加一个功能 class。

正在修补 LtvVerification

iText 5 LtvVerification class 仅提供 addVerification 接受签名字段名称的方法。对于未绑定到表单字段的签名,我们也需要这些方法的功能,例如用于 OCSP 响应签名。为此,我添加了该方法的以下重载:

public boolean addVerification(PdfName signatureHash, Collection<byte[]> ocsps, Collection<byte[]> crls, Collection<byte[]> certs) throws IOException, GeneralSecurityException {
    if (used)
        throw new IllegalStateException(MessageLocalization.getComposedMessage("verification.already.output"));
    ValidationData vd = new ValidationData();
    if (ocsps != null) {
        for (byte[] ocsp : ocsps) {
            vd.ocsps.add(buildOCSPResponse(ocsp));
        }
    }
    if (crls != null) {
        for (byte[] crl : crls) {
            vd.crls.add(crl);
        }
    }
    if (certs != null) {
        for (byte[] cert : certs) {
            vd.certs.add(cert);
        }
    }
    validated.put(signatureHash, vd);
    return true;
}

此外,最终 VRI 词典中的(根据规范可选)时间条目是必需的(但请参阅下面的 PPS)。因此,我在 outputDss 方法中添加了一行,如下所示:

...
if (ocsp.size() > 0)
    vri.put(PdfName.OCSP, writer.addToBody(ocsp, false).getIndirectReference());
if (crl.size() > 0)
    vri.put(PdfName.CRL, writer.addToBody(crl, false).getIndirectReference());
if (cert.size() > 0)
    vri.put(PdfName.CERT, writer.addToBody(cert, false).getIndirectReference());
// v--- added line
vri.put(PdfName.TU, new PdfDate());
// ^--- added line
vrim.put(vkey, writer.addToBody(vri, false).getIndirectReference());
...

一些低级辅助方法

需要一些对安全原语进行操作的辅助方法。这些方法大多是从现有的 iText classes(不能按原样使用,因为它们是私有的)或从那里的代码派生的:

static X509Certificate getOcspSignerCertificate(byte[] basicResponseBytes) throws CertificateException, OCSPException, OperatorCreationException {
    JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider(BouncyCastleProvider.PROVIDER_NAME);
    BasicOCSPResponse borRaw = BasicOCSPResponse.getInstance(basicResponseBytes);
    BasicOCSPResp bor = new BasicOCSPResp(borRaw);

    for (final X509CertificateHolder x509CertificateHolder : bor.getCerts()) {
        X509Certificate x509Certificate = converter.getCertificate(x509CertificateHolder);

        JcaContentVerifierProviderBuilder jcaContentVerifierProviderBuilder = new JcaContentVerifierProviderBuilder();
        jcaContentVerifierProviderBuilder.setProvider(BouncyCastleProvider.PROVIDER_NAME);
        final PublicKey publicKey = x509Certificate.getPublicKey();
        ContentVerifierProvider contentVerifierProvider = jcaContentVerifierProviderBuilder.build(publicKey);

        if (bor.isSignatureValid(contentVerifierProvider))
            return x509Certificate;
    }

    return null;
}

static PdfName getOcspSignatureKey(byte[] basicResponseBytes) throws NoSuchAlgorithmException, IOException {
    BasicOCSPResponse basicResponse = BasicOCSPResponse.getInstance(basicResponseBytes);
    byte[] signatureBytes = basicResponse.getSignature().getBytes();
    DEROctetString octetString = new DEROctetString(signatureBytes);
    byte[] octetBytes = octetString.getEncoded();
    byte[] octetHash = hashBytesSha1(octetBytes);
    PdfName octetName = new PdfName(Utilities.convertToHex(octetHash));
    return octetName;
}

static PdfName getCrlSignatureKey(byte[] crlBytes) throws NoSuchAlgorithmException, IOException, CRLException, CertificateException {
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    X509CRL crl = (X509CRL)cf.generateCRL(new ByteArrayInputStream(crlBytes));
    byte[] signatureBytes = crl.getSignature();
    DEROctetString octetString = new DEROctetString(signatureBytes);
    byte[] octetBytes = octetString.getEncoded();
    byte[] octetHash = hashBytesSha1(octetBytes);
    PdfName octetName = new PdfName(Utilities.convertToHex(octetHash));
    return octetName;
}

static X509Certificate getIssuerCertificate(X509Certificate certificate) throws IOException, StreamParsingException {
    String url = getCACURL(certificate);
    if (url != null && url.length() > 0) {
        HttpURLConnection con = (HttpURLConnection)new URL(url).openConnection();
        if (con.getResponseCode() / 100 != 2) {
            throw new IOException(MessageLocalization.getComposedMessage("invalid.http.response.1", con.getResponseCode()));
        }
        InputStream inp = (InputStream) con.getContent();
        byte[] buf = new byte[1024];
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        while (true) {
            int n = inp.read(buf, 0, buf.length);
            if (n <= 0)
                break;
            bout.write(buf, 0, n);
        }
        inp.close();

        X509CertParser parser = new X509CertParser();
        parser.engineInit(new ByteArrayInputStream(bout.toByteArray()));
        return (X509Certificate) parser.engineRead();
    }
    return null;
}

static String getCACURL(X509Certificate certificate) {
    ASN1Primitive obj;
    try {
        obj = getExtensionValue(certificate, Extension.authorityInfoAccess.getId());
        if (obj == null) {
            return null;
        }
        ASN1Sequence AccessDescriptions = (ASN1Sequence) obj;
        for (int i = 0; i < AccessDescriptions.size(); i++) {
            ASN1Sequence AccessDescription = (ASN1Sequence) AccessDescriptions.getObjectAt(i);
            if ( AccessDescription.size() != 2 ) {
                continue;
            }
            else if (AccessDescription.getObjectAt(0) instanceof ASN1ObjectIdentifier) {
                ASN1ObjectIdentifier id = (ASN1ObjectIdentifier)AccessDescription.getObjectAt(0);
                if ("1.3.6.1.5.5.7.48.2".equals(id.getId())) {
                    ASN1Primitive description = (ASN1Primitive)AccessDescription.getObjectAt(1);
                    String AccessLocation =  getStringFromGeneralName(description);
                    if (AccessLocation == null) {
                        return "" ;
                    }
                    else {
                        return AccessLocation ;
                    }
                }
            }
        }
    } catch (IOException e) {
        return null;
    }
    return null;
}

static ASN1Primitive getExtensionValue(X509Certificate certificate, String oid) throws IOException {
    byte[] bytes = certificate.getExtensionValue(oid);
    if (bytes == null) {
        return null;
    }
    ASN1InputStream aIn = new ASN1InputStream(new ByteArrayInputStream(bytes));
    ASN1OctetString octs = (ASN1OctetString) aIn.readObject();
    aIn = new ASN1InputStream(new ByteArrayInputStream(octs.getOctets()));
    return aIn.readObject();
}

static String getStringFromGeneralName(ASN1Primitive names) throws IOException {
    ASN1TaggedObject taggedObject = (ASN1TaggedObject) names ;
    return new String(ASN1OctetString.getInstance(taggedObject, false).getOctets(), "ISO-8859-1");
}

static byte[] hashBytesSha1(byte[] b) throws NoSuchAlgorithmException {
    MessageDigest sh = MessageDigest.getInstance("SHA1");
    return sh.digest(b);
}

(如 MakeLtvEnabled

它们还没有优化,当然可以使它们更高效、更优雅。

添加 LTV 信息

基于这些添加和帮助,可以使用此方法添加启用 LTV 的签名所需的 LTV 信息 makeLtvEnabled:

public void makeLtvEnabled(PdfStamper stp, OcspClient ocspClient, CrlClient crlClient) throws IOException, GeneralSecurityException, StreamParsingException, OperatorCreationException, OCSPException {
    stp.getWriter().addDeveloperExtension(new PdfDeveloperExtension(PdfName.ADBE, new PdfName("1.7"), 8));
    LtvVerification v = stp.getLtvVerification();
    AcroFields fields = stp.getAcroFields();

    Map<PdfName, X509Certificate> moreToCheck = new HashMap<>();

    ArrayList<String> names = fields.getSignatureNames();
    for (String name : names)
    {
        PdfPKCS7 pdfPKCS7 = fields.verifySignature(name);
        List<X509Certificate> certificatesToCheck = new ArrayList<>();
        certificatesToCheck.add(pdfPKCS7.getSigningCertificate());
        while (!certificatesToCheck.isEmpty()) {
            X509Certificate certificate = certificatesToCheck.remove(0);
            addLtvForChain(certificate, ocspClient, crlClient,
                    (ocsps, crls, certs) -> {
                        try {
                            v.addVerification(name, ocsps, crls, certs);
                        } catch (IOException | GeneralSecurityException e) {
                            e.printStackTrace();
                        }
                    },
                    moreToCheck::put
            );
        }
    }

    while (!moreToCheck.isEmpty()) {
        PdfName key = moreToCheck.keySet().iterator().next();
        X509Certificate certificate = moreToCheck.remove(key);
        addLtvForChain(certificate, ocspClient, crlClient,
                (ocsps, crls, certs) -> {
                    try {
                        v.addVerification(key, ocsps, crls, certs);
                    } catch (IOException | GeneralSecurityException e) {
                        e.printStackTrace();
                    }
                },
                moreToCheck::put
        );
    }
}

void addLtvForChain(X509Certificate certificate, OcspClient ocspClient, CrlClient crlClient, VriAdder vriAdder,
        BiConsumer<PdfName, X509Certificate> moreSignersAndCertificates) throws GeneralSecurityException, IOException, StreamParsingException, OperatorCreationException, OCSPException {
    List<byte[]> ocspResponses = new ArrayList<>();
    List<byte[]> crls = new ArrayList<>();
    List<byte[]> certs = new ArrayList<>();

    while (certificate != null) {
        System.out.println(certificate.getSubjectX500Principal().getName());
        X509Certificate issuer = getIssuerCertificate(certificate);
        certs.add(certificate.getEncoded());
        byte[] ocspResponse = ocspClient.getEncoded(certificate, issuer, null);
        if (ocspResponse != null) {
            System.out.println("  with OCSP response");
            ocspResponses.add(ocspResponse);
            X509Certificate ocspSigner = getOcspSignerCertificate(ocspResponse);
            if (ocspSigner != null) {
                System.out.printf("  signed by %s\n", ocspSigner.getSubjectX500Principal().getName());
            }
            moreSignersAndCertificates.accept(getOcspSignatureKey(ocspResponse), ocspSigner);
        } else {
           Collection<byte[]> crl = crlClient.getEncoded(certificate, null);
           if (crl != null && !crl.isEmpty()) {
               System.out.printf("  with %s CRLs\n", crl.size());
               crls.addAll(crl);
               for (byte[] crlBytes : crl) {
                   moreSignersAndCertificates.accept(getCrlSignatureKey(crlBytes), null);
               }
           }
        }
        certificate = issuer;
    }

    vriAdder.accept(ocspResponses, crls, certs);
}

interface VriAdder {
    void accept(Collection<byte[]> ocsps, Collection<byte[]> crls, Collection<byte[]> certs);
}

(MakeLtvEnabled as makeLtvEnabledV2)

用法示例

对于 INPUT_PDF 处的签名 PDF 和结果输出流 RESULT_STREAM,您可以像这样使用上面的方法:

PdfReader pdfReader = new PdfReader(INPUT_PDF);
PdfStamper pdfStamper = new PdfStamper(pdfReader, RESULT_STREAM, (char)0, true);

OcspClient ocsp = new OcspClientBouncyCastle();
CrlClient crl = new CrlClientOnline();
makeLtvEnabledV2(pdfStamper, ocsp, crl);

pdfStamper.close();

(MakeLtvEnabled测试方法testV2)

限制

以上方法仅在一些简化限制下有效,特别是:

  • 忽略签名时间戳,
  • 假定检索到的 CRL 是直接且完整的,
  • 假定完整的证书链可以使用 AIA 条目构建。

如果您不能接受这些限制,您可以相应地改进代码。

一种使用自己的实用程序的方法class

为了避免必须修补 iText class,此方法采用上述方法中的所需代码和 iText 签名 API 中的 LtvVerification class 并合并全部放入一个新的实用程序 class。 class LTV 可以启用文档而不需要修补的 iText 版本。

AdobeLtvEnablingclass

这个 class 将上面的代码和一些 LtvVerification 代码组合成实用程序 class 用于 LTV 启用文档。

不幸的是,将其复制到此处会使消息大小超出堆栈溢出的 30000 个字符限制。您可以从 github 检索代码,但是:

AdobeLtvEnabling.java

用法示例

对于 INPUT_PDF 处的签名 PDF 和结果输出流 RESULT_STREAM,您可以像这样使用上面的 class:

PdfReader pdfReader = new PdfReader(INPUT_PDF);
PdfStamper pdfStamper = new PdfStamper(pdfReader, RESULT_STREAM, (char)0, true);

AdobeLtvEnabling adobeLtvEnabling = new AdobeLtvEnabling(pdfStamper);
OcspClient ocsp = new OcspClientBouncyCastle();
CrlClient crl = new CrlClientOnline();
adobeLtvEnabling.enable(ocsp, crl);

pdfStamper.close();

(MakeLtvEnabled测试方法testV3)

限制

由于此实用程序 class 仅重新打包第一种方法的代码,因此适用相同的限制。

幕后花絮

如开头所述,Adobe 告诉我们有关 "LTV enabled" 签名配置文件的全部内容是

LTV enabled means that all information necessary to validate the file (minus root certs) is contained within

但他们没有告诉我们他们希望将信息嵌入到文件中的准确程度。

起初我只是收集了所有这些信息,并确保将其添加到 PDF 的适用文档安全存储词典中(CertsOCSPs,和 CRL).

但即使验证文件所需的所有信息(减去根证书)都包含在中,Adobe Acrobat 也没有考虑文件"LTV enabled"。

然后我使用 Adob​​e Acrobat 启用了 LTV 并分析了差异。事实证明,还需要以下额外数据(但请参阅下面的 PPS):

  1. 对于每个 OCSP 响应的签名,Adobe Acrobat 需要存在相应的 VRI 字典。在 OP 的示例 PDF 中,此 VRI 字典根本不需要包含任何证书、CRL 或 OCSP 响应,但需要包含 VRI 字典。

    相比之下,这对于 CRL 的签名来说不是所必需的。这看起来有点武断。

    根据 ISO 32000-2 和 ETSI EN 319 142-1 的规范,使用这些 VRI 字典纯粹是 可选.对于 PAdES BASELINE 签名,甚至有一个建议 against 使用 VRI 字典!

  2. Adobe Acrobat 期望每个 VRI 词典都包含一个 TU 条目,记录相应 VRI字典。 (可能 TS 也可以,我还没有测试过)。

    根据规范,ISO 32000-2 和 ETSI EN 319 142-1,使用这些 TU 条目纯粹是 可选.对于 PAdES 签名,甚至有一个建议 against 使用 TUTS 条目!

因此,默认情况下应用程序根据 PDF 规范添加的 LTV 信息不会产生 Adob​​e Acrobat 报告的 "LTV enabled" 签名也就不足为奇了。

PS

显然,我必须在 Adob​​e Acrobat 中添加对某些证书的信任,以使其完全考虑 OP 文档 "LTV enabled" 的上述代码的结果。我选择了根证书"CA RAIZ NACIONAL - COSTA RICA v2".

PPS (2020-03-02)

显然同时 Adob​​e Acrobat 不再需要 VRI 词典(更不用说 TU 时间戳)来考虑撤销信息测试启用 LTV 状态时的 DSS,请参阅 的 "Optional elements in the DSS" 部分。

因此,上述解决方案很可能可以稍微简化。